package haxeframe.views;

import box2D.dynamics.B2Body;
import box2D.dynamics.B2DebugDraw;
import box2D.dynamics.B2World;
import box2D.common.math.B2Vec2;

import flash.errors.TypeError;
import flash.Lib;
import flash.display.Sprite;
import flash.events.Event;
import flash.system.System;

import haxe.ds.IntMap;
import haxe.Json;

import haxe.Timer;
import haxeframe.Globals;
import haxeframe.physics.Assets;
import haxeframe.physics.Layer;
import haxeframe.physics.Symbol;
import haxeframe.physics.SymbolSettings;

class ViewPhysics extends View
{
	public var paused:Bool;
	public var debug:Bool;

	public var layoutXml:Xml;
	public var layoutJson:Json;

	private var xmlLayers:Iterator<Xml>;

	private var buildLayer:Xml;
	private var buildInstances:Iterator<Xml>;

	private var currentInstance:Int;
	private var totalInstances:Int;
	public var buildProgress:Float = 0;

	private var renderConstant:Array<Symbol>;
	private var renderDynamic:Map<Layer, IntMap<Array<Symbol>>>;
	
	private var layout:Map<Layer, IntMap<Array<SymbolSettings>>>;
	
	public inline static var LAYOUT_SORT_X:String = 'x';
	public inline static var LAYOUT_SORT_Y:String = 'y';

	private var layoutSort:String = LAYOUT_SORT_X;

	private var world:B2World;
	private var scale:Float = 30;
	private var gravity:B2Vec2;
	private var gravityX:Float = 0;
	private var gravityY:Float = 10;

	private var focus:Symbol;

	public var camera:Sprite;
	private var cameraXMin:Float = -100000;
	private var cameraXMax:Float = 100000;
	private var cameraYMin:Float = -100000;
	private var cameraYMax:Float = 100000;

	private var followFactorX:Float = 1;
	private var followFactorY:Float = 1;

	private var renderMargin:Int = 512;

	public var progress:Float = 0;

	public var worldSprite:Sprite;
	private var debugSprite:Sprite;
	private var debugDraw:B2DebugDraw;

	private var timeStepOverride:Float = 0;

	private var iterations:Int = 10;
	private var allowSleep:Bool = true;

	private var now:Int;
	private var then:Int;
	private var elapsed:Int;

	private var updateRate:Int = 16;
	private var updateTimer:Timer;

	public var symbols:Xml;

	private var lastCameraPosition:Float = 0;
	public var buildItemsPerFrame:Int = 3;
	
	public function new()
	{
 		super ();
	}

	public function pause():Void
	{
		paused = true;
	}

	public function unPause():Void
	{
		paused = false;
		elapsed = 0;
	}

	public override function init():Void
	{		
		dispose();

		super.init();

		if(symbols != null)
			Assets.addSymbols(symbols, world, scale);

		camera = new Sprite();

		currentInstance = 0;
		totalInstances = 0;

		for(layerXml in layoutXml.elementsNamed('layer')){
			for(instanceXml in layerXml.elementsNamed('instance')){
				totalInstances++;
			}
		}

		paused = true;

		addEventListener(Event.REMOVED_FROM_STAGE, onRemoved);

		gravity = new B2Vec2(gravityX, gravityY);
		world = new B2World(gravity, allowSleep);

		worldSprite = new Sprite();
		addChild(worldSprite);

		layout = new Map<Layer, IntMap<Array<SymbolSettings>>>();
		renderConstant = new Array<Symbol>();
		renderDynamic = new Map<Layer, IntMap<Array<Symbol>>>();

		xmlLayers = layoutXml.elementsNamed('layer');
		// Timer.delay(build, 1000);
		addEventListener(Event.ENTER_FRAME, build);
	}



	private function build(event:Event=null):Void
	{
		if(layoutJson != null){

			// TODO: Add Json support

		} else if(layoutXml != null){

			var layerMap:IntMap<Array<SymbolSettings>>;
			var renderMap:IntMap<Array<Symbol>>;
			var layer:Layer;

			var nearLimit:Float = 0;
			var farLimit:Float = stage.stageWidth/Globals.SCALE;

			for(i in 0...buildItemsPerFrame){

				if(buildLayer == null || !buildInstances.hasNext()){
					if(!xmlLayers.hasNext()){
						onBuildComplete();
						return;
					} else {
						buildLayer = xmlLayers.next();
						layer = new Layer();
						layer.name = buildLayer.get('name');
						layer.xFactor = Std.parseFloat(buildLayer.get('xFactor'));
						layer.yFactor = Std.parseFloat(buildLayer.get('yFactor'));
						worldSprite.addChild(layer);
						layerMap = new IntMap<Array<SymbolSettings>>();
						renderMap = new IntMap<Array<Symbol>>();
						buildInstances = buildLayer.elementsNamed('instance');
					}
				} else {
					layer = cast(worldSprite.getChildByName(buildLayer.get('name')), Layer);
					layerMap = layout.get(layer);
					renderMap = renderDynamic.get(layer);
				}

				if(!buildInstances.hasNext()){
					// Timer.delay(build, 1);
					return;
				}

				if(buildInstances.hasNext())
				{
					currentInstance++;

					var instanceXml:Xml = buildInstances.next();

					var settings:SymbolSettings = new SymbolSettings();
					settings.x = Std.parseFloat(instanceXml.get('x'));
					settings.y = Std.parseFloat(instanceXml.get('y'));
					settings.scaleX = Std.parseFloat(instanceXml.get('scaleX'));
					settings.scaleY = Std.parseFloat(instanceXml.get('scaleY'));
					settings.rotation = Std.parseFloat(instanceXml.get('rotation'));
					settings.bodyType = Std.parseInt(instanceXml.get('bodyType'));
					settings.symbolName = instanceXml.get('symbol');
					settings.name = instanceXml.get('name');

					if(instanceXml.exists('maskBits')) settings.maskBits = Std.parseInt(instanceXml.get('maskBits'));

					if(instanceXml.get('focus') == 'true') settings.focus = true;
					if(instanceXml.get('bullet') == 'true') settings.bullet = true;
					if(instanceXml.get('constant') == 'true') settings.constant = true;

					/*
						If the instance is flagged "constant", we'll add 
						it to the renderConstant array instead of adding 
						it to the layout.
					*/
					if(settings.constant && settings.focus){

						var instance:Symbol = Assets.getSymbol(settings.symbolName);

						if(instance == null){
							trace('null instance : '+settings.symbolName);
							// build();
							continue;
						}

						if(settings.name != null){
							instance.name = settings.name;
						}	
						instance.setPosition(settings.x, settings.y);
						instance.scaleX = settings.scaleX;
						instance.scaleY = settings.scaleY;
						instance.rotation = settings.rotation;
						instance.world = world;
						instance.bodyType = settings.bodyType;

						instance.init();

						if(instance.body == null) instance.reset();

						if(instance.body != null){
							instance.body.setPosition(new B2Vec2(settings.x/scale,settings.y/scale));
							instance.body.setAngle(instance.rotation);
							instance.body.setActive(true);
							instance.body.setAwake(true);
							if(settings.bullet)
								instance.body.setBullet(true);
						}

						layer.addChild(instance);

						if(settings.focus)
							focus = instance;

						renderConstant.push(instance);

					} else {

						var position:Int = 0;

						if(layoutSort == LAYOUT_SORT_X){
							position = Math.round(settings.x);
						} else if(layoutSort == LAYOUT_SORT_Y){
							position = Math.round(settings.y);
						}

						if(renderMap.get(position) == null) renderMap.set(position, new Array<Symbol>());
						if(layerMap.get(position) == null) layerMap.set(position, new Array<SymbolSettings>());
						layerMap.get(position).push(settings);

						var thisRender:Array<Symbol> = renderMap.get(position);

						if(position + renderMargin >= nearLimit && position - renderMargin <= farLimit && buildProgress < .5)
						{
							var instance:Symbol = Assets.getSymbol(settings.symbolName);

							if(instance == null){
								trace('null instance : '+settings.symbolName);
								// build();
								continue;
							}

							if(settings.name != null){
								instance.name = settings.name;
							}	
							instance.setPosition(settings.x, settings.y);
							instance.scaleX = settings.scaleX;
							instance.scaleY = settings.scaleY;
							instance.rotation = settings.rotation;
							instance.world = world;
							instance.bodyType = settings.bodyType;

							if(instance.formerParent == null) 
								instance.init();

							instance.update();

							if(instance.body == null) instance.reset();

							if(instance.body != null){
								instance.body.setPosition(new B2Vec2(settings.x/scale,settings.y/scale));
								instance.body.setAngle(instance.rotation);
								instance.body.setActive(true);
								instance.body.setAwake(true);
								if(settings.bullet)
									instance.body.setBullet(true);
							}

							layer.addChild(instance);

							if(settings.focus)
								focus = instance;

							thisRender.push(instance);
						}
					}

				}
				layout.set(layer, layerMap);
				renderDynamic.set(layer, renderMap);
			}

		}

		buildProgress = currentInstance / totalInstances;

		// build();

	}

	private function onBuildComplete():Void
	{
		if(debug) initDebug();

		removeEventListener(Event.ENTER_FRAME, build);
		// addEventListener(Event.ENTER_FRAME, update);
		updateTimer = Timer.delay(update, updateRate);

		paused = false;
	}

	private function initDebug():Void
	{
		debugSprite = new Sprite();
		debugSprite.mouseEnabled = false;
		debugSprite.alpha = .5;
		addChild (debugSprite);
		
		debugDraw = new B2DebugDraw ();
		debugDraw.setSprite(debugSprite);
		debugDraw.setDrawScale(scale*Globals.SCALE);
		debugDraw.setFlags (B2DebugDraw.e_shapeBit);
		
		world.setDebugDraw (debugDraw);
	}

	private function update():Void
	{
		now = Lib.getTimer();
		elapsed = now - then;
		then = now;

		if(updateTimer != null) updateTimer.stop();
		updateTimer = Timer.delay(update, updateRate);

		if(paused) return;

		if(world != null){

			if(timeStepOverride != 0){
				world.step(timeStepOverride, iterations, iterations);
			} else {
				if(elapsed > updateRate)
					world.step(elapsed/1000, iterations, iterations);
				else
					world.step(updateRate/1000, iterations, iterations);
			}
				
			world.clearForces();

			var cameraMovement:Float = 0;

			if(layoutSort == LAYOUT_SORT_X){
				cameraMovement = camera.x - lastCameraPosition;
			} else if(layoutSort == LAYOUT_SORT_Y){
				cameraMovement = camera.y - lastCameraPosition;
			}	

			var rendering:Int = 0;
			var checking:Int = 0;

			for (layer in layout.keys())
			{				
				var layerMap:IntMap<Array<SymbolSettings>> = layout.get(layer);

				if(layerMap == null) continue;

				var nearLimit:Float = 0;
				var farLimit:Float = 0;

				if(layoutSort == LAYOUT_SORT_X){
					nearLimit = (-worldSprite.x * layer.xFactor)/Globals.SCALE;
					farLimit = nearLimit + stage.stageWidth/Globals.SCALE;
				} else if(layoutSort == LAYOUT_SORT_Y){
					nearLimit = (-worldSprite.y * layer.yFactor)/Globals.SCALE;
					farLimit = nearLimit + stage.stageHeight/Globals.SCALE;
				}

				var renderMap:IntMap<Array<Symbol>> = renderDynamic.get(layer);

				for(position in layerMap.keys())
				{
					++checking;

					var thisLayout:Array<SymbolSettings> = layerMap.get(position);

					// If there is never anything at this position for this layer, don't do anything
					if(thisLayout == null) continue;
					if(thisLayout.length == 0) continue;

					var thisRender:Array<Symbol> = renderMap.get(position);

					if((position + renderMargin < nearLimit || position - renderMargin > farLimit))
					{
						// We're not supposed to render this anyway, so continue
						if(thisRender == null) continue;

						for(instance in thisRender)
						{
							instance.removeFromParent();
							thisRender.remove(instance);
							Assets.returnSymbol(instance.symbolName, instance);
						}

					} else {

						if(thisRender.length == 0)
						{
							for(s in 0...thisLayout.length)
							{
								var settings:SymbolSettings = thisLayout[s];
								if(settings == null) continue;
								var instance:Symbol = Assets.getSymbol(settings.symbolName);

								if(instance == null) continue;

								if(settings.name != null){
									instance.name = settings.name;
									// trace('instance.name : '+instance.name);
								}	
								instance.setPosition(settings.x, settings.y);
								instance.scaleX = settings.scaleX;
								instance.scaleY = settings.scaleY;
								instance.rotation = settings.rotation;
								instance.world = world;
								instance.bodyType = settings.bodyType;

								if(instance.formerParent == null) 
									instance.init();

								instance.update();

								if(instance.body != null){
									instance.body.setPosition(new B2Vec2(settings.x/scale,settings.y/scale));
									instance.body.setAngle(instance.rotation);
									instance.body.setActive(true);
									instance.body.setAwake(true);
									if(settings.bullet)
										instance.body.setBullet(true);
								}

								if(cameraMovement >= 0){
									layer.addChildAt(instance, 0);
								} else {
									layer.addChild(instance);
								}

								if(settings.focus)
									focus = instance;

								if(settings.constant){
									renderConstant.push(instance);
									thisLayout.remove(settings);
								} else {
									thisRender.push(instance);
								}

								++rendering;
							}

						} else {

							for(i in 0...thisRender.length){
								var instance:Symbol = thisRender[i];	
								if(instance == null) continue;
								if(instance.remove){
									instance.removeFromParent();
									thisRender.remove(instance);
									thisLayout.remove(thisLayout[i]);
									Assets.returnSymbol(instance.symbolName, instance);
								} else {
									instance.update();
									if(instance.body == null) continue;
									var pos:B2Vec2 = instance.body.getPosition();
									instance.setPosition(pos.x * scale, pos.y * scale);
									instance.rotation = instance.body.getAngle();
									++rendering;
								}
							}

						}
					}
				}
			}

			for(instance in renderConstant){
				if(instance == null) continue;
				
				if(instance.remove){
					instance.removeFromParent();
					if(instance.body != null)
						instance.body.setActive(false);
					Assets.returnSymbol(instance.symbolName, instance);
					renderConstant.remove(instance);
					continue;
				}
				if(instance.body == null){
					instance.init();
				}
				instance.update();
				if(instance.body != null){
					instance.body.setActive(true);
					instance.setPosition(instance.body.getPosition().x * scale, instance.body.getPosition().y * scale);
					instance.rotation = instance.body.getAngle();
				}
			}

			lastCameraPosition = (layoutSort == LAYOUT_SORT_X) ? camera.x : camera.y;
			updateCamera();

			for (layer in layout.keys())
			{
				// Update layer scale and position
				if (layer.inverseScale)
				{
					layer.scaleX = 1 / worldSprite.scaleX;
					layer.scaleY = 1 / worldSprite.scaleY;
				}
				layer.x = -(worldSprite.x * (1 / worldSprite.scaleX) *  (1 - layer.xFactor));
				layer.y = -(worldSprite.y * (1 / worldSprite.scaleY) * (1 - layer.yFactor));

			}

			if(debug){
				world.drawDebugData();
				debugSprite.x = worldSprite.x;
				debugSprite.y = worldSprite.y;
				debugSprite.scaleX = worldSprite.scaleX;
				debugSprite.scaleY = worldSprite.scaleY;
				debugSprite.parent.addChild(debugSprite);
			}
		}
	}

	private function updateCamera():Void
	{
		if(focus != null)
		{
			var position:Dynamic = focus.getPosition();
			camera.x += (position.x*Globals.SCALE - camera.x) * followFactorX;
			camera.y += (position.y*Globals.SCALE - camera.y) * followFactorY;
		}
		worldSprite.x = -camera.x + stage.stageWidth*.5;
		worldSprite.y = -camera.y + stage.stageHeight*.5;

	}

	private function onRemoved(event:Event):Void
	{
		removeEventListener(Event.ENTER_FRAME, build);
		removeEventListener(Event.REMOVED_FROM_STAGE, onRemoved);
		if(updateTimer != null) updateTimer.stop();
		// removeEventListener(Event.ENTER_FRAME, update);
	}

	private function clearWorld():Void
	{
		if(world != null){
			var bb:B2Body = world.getBodyList();
			while(bb != null){
				world.destroyBody(bb);
				bb = world.getBodyList();
			}
		}
	}

	private function dispose():Void
	{
		while(numChildren > 0)
			removeChildAt(0);

		worldSprite = null;
		debugSprite = null;

		clearWorld();

		world = null;
	}
	
}